<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Begin Jekyll SEO tag v2.7.1 -->
<title>持久化在说什么 | 微光</title>
<meta name="generator" content="Jekyll v4.2.1">
<meta property="og:title" content="持久化在说什么">
<meta property="og:locale" content="en_US">
<meta name="description" content="本文对持久化的含义、过程，以及持久化过程中可能会遇到的问题做一个简单介绍。">
<meta property="og:description" content="本文对持久化的含义、过程，以及持久化过程中可能会遇到的问题做一个简单介绍。">
<link rel="canonical" href="/redis/persist/durability.html">
<meta property="og:url" content="/redis/persist/durability.html">
<meta property="og:site_name" content="微光">
<meta property="og:type" content="article">
<meta property="article:published_time" content="2022-01-11T00:00:00+08:00">
<meta name="twitter:card" content="summary">
<meta property="twitter:title" content="持久化在说什么">
<script type="application/ld+json">
{"description":"本文对持久化的含义、过程，以及持久化过程中可能会遇到的问题做一个简单介绍。","url":"/redis/persist/durability.html","headline":"持久化在说什么","dateModified":"2022-01-11T00:00:00+08:00","datePublished":"2022-01-11T00:00:00+08:00","@type":"BlogPosting","mainEntityOfPage":{"@type":"WebPage","@id":"/redis/persist/durability.html"},"@context":"https://schema.org"}</script>
<!-- End Jekyll SEO tag -->
<link rel="stylesheet" href="/assets/css/normalize.css">
  <link rel="stylesheet" href="/assets/css/style.css">
<link type="application/atom+xml" rel="alternate" href="/feed.xml" title="微光">
</head>
<body>
<header class="site-header">

  <div class="wrapper">
<a class="site-title" rel="author" href="/">微光</a><nav class="site-nav">
        <input type="checkbox" id="nav-trigger" class="nav-trigger">
        <label for="nav-trigger">
          <span class="menu-icon">
            <svg viewbox="0 0 18 15" width="18px" height="15px">
              <path d="M18,1.484c0,0.82-0.665,1.484-1.484,1.484H1.484C0.665,2.969,0,2.304,0,1.484l0,0C0,0.665,0.665,0,1.484,0 h15.032C17.335,0,18,0.665,18,1.484L18,1.484z M18,7.516C18,8.335,17.335,9,16.516,9H1.484C0.665,9,0,8.335,0,7.516l0,0 c0-0.82,0.665-1.484,1.484-1.484h15.032C17.335,6.031,18,6.696,18,7.516L18,7.516z M18,13.516C18,14.335,17.335,15,16.516,15H1.484 C0.665,15,0,14.335,0,13.516l0,0c0-0.82,0.665-1.483,1.484-1.483h15.032C17.335,12.031,18,12.695,18,13.516L18,13.516z"></path>
            </svg>
          </span>
        </label>

        <div class="trigger">
<a class="page-link" href="/about/">About</a><a class="page-link" href="/doc/main">DocMain</a><a class="page-link" href="/sites.html">Sites</a>
</div>
      </nav>
</div>
</header>
<div class="x-collections-bar">
  <a href="/">Home</a>
  
    
      <a href="/algri/index">算法</a>
    
  
    
      <a href="/java/index">Java</a>
    
  
    
  
    
  
    
      <a href="/redis/index">Redis</a>
    
  
</div>

<div class="x-content-side-layout x-container">
  <div class="x-left">
    <h3>本文目录</h3>
    <ul id="toc" class="section-nav">
<li class="toc-entry toc-h2"><a href="#%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E4%B8%8E%E7%A3%81%E7%9B%98">操作系统与磁盘</a></li>
<li class="toc-entry toc-h2"><a href="#%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E5%86%99%E6%93%8D%E4%BD%9C%E6%98%AF%E5%AE%89%E5%85%A8%E7%9A%84">什么时候写操作是安全的？</a></li>
<li class="toc-entry toc-h2"><a href="#posix-api">POSIX API</a></li>
<li class="toc-entry toc-h2"><a href="#%E6%88%91%E4%BB%AC%E4%B8%8D%E8%83%BD%E6%8E%A7%E5%88%B6%E4%BB%80%E4%B9%88">我们不能控制什么？</a></li>
<li class="toc-entry toc-h2"><a href="#%E6%95%B0%E6%8D%AE%E5%B4%A9%E6%BA%83">数据崩溃</a></li>
<li class="toc-entry toc-h2"><a href="#%E5%8F%82%E8%80%83">参考</a></li>
</ul>
  </div>
  <div class="x-content">
    <h1><strong>Redis - 持久化在说什么</strong></h1>
    <small>2022-01-11 00:00:00 +0800</small>
    
    <article class="x-article"><p>本文对持久化的含义、过程，以及持久化过程中可能会遇到的问题做一个简单介绍。</p>

<h2 id="操作系统与磁盘">
<a class="anchor" href="#%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E4%B8%8E%E7%A3%81%E7%9B%98" aria-hidden="true"><span class="octicon octicon-link"></span></a>操作系统与磁盘</h2>

<p>我们首先要考虑的事情是，我们对数据库中的术语——“<strong>持久性</strong>”（<em>durability</em>）应该怀有怎样的期待。为此，我们需要了解一个简单的写操作期间究竟发生了什么：</p>

<ol>
  <li>客户端发送一个写命令到数据库（这时数据在客户端内存中）；</li>
  <li>数据库收到写请求（这时数据在数据库服务器内存中）；</li>
  <li>数据库发起系统调用把数据写到磁盘上（这时数据在操作系统的内核 buffer 中）；</li>
  <li>操作系统把写 buffer 发送到磁盘控制器（这时数据在磁盘缓存中）；</li>
  <li>磁盘控制器真正把数据写到物理媒介上（磁盘、闪存芯片等等）</li>
</ol>

<blockquote>
  <p>注意：上面的描述有些<strong>过于简化</strong>了，因为还有很多层的缓存和 buffer 没有提。</p>

  <p>第 2 步经常被实现为数据库内部的一个复杂的缓存系统，有时写操作会被不同的线程或进程处理。不管怎样，数据库都必须把数据写到磁盘上，这才是我们的视角中重要的事。换句话说，在某个时间点，数据会被传输到操作系统内核（第 3 步）。</p>

  <p>另一个大的简化是第 3 步中的细节，现实远比这复杂。大多数先进的的内核都实现了不同层级的缓存，通常有文件系统层级的缓存（Linux 中叫页面缓存），和一个更小的 <em>buffer 缓存</em>，即用一个 buffer 临时存储那些等着被提交到磁盘的数据。使用特殊的 API 可以绕过上面两个缓存。不过从我们的视角来看，我们可以认为这是一个唯一的不透明缓存（即我们不知道细节）。当数据库已经实现了自己的缓存时，页面缓存通常会被禁用，以避免数据库和内核在同一时间尝试做相同的工作（导致不好的结果）。Buffer 缓存通常不会被关掉，因为关掉的话，每次写文件都会导致数据被提交到磁盘，这对大多数应用程序来说都太慢了。</p>

  <p>数据库通常仅在确实需要时，才会发起系统调用把 buffer 缓存中的数据提交到磁盘。</p>
</blockquote>

<h2 id="什么时候写操作是安全的">
<a class="anchor" href="#%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E5%86%99%E6%93%8D%E4%BD%9C%E6%98%AF%E5%AE%89%E5%85%A8%E7%9A%84" aria-hidden="true"><span class="octicon octicon-link"></span></a>什么时候写操作是安全的？</h2>

<p>如果我们仅考虑数据库软件的失败（比如进程系统管理员杀掉，或者崩溃了）而不触及内核，那么只要第 3 步成功完成，我们就可以认为写是安全的。此步骤之后，即使数据库进程崩溃了，内核依旧会把数据传输到磁盘控制器。</p>

<p>如果我们考虑更加悲催的事件，比如断电，那么只有第 5 步完成才算安全，即数据真的被传输到物理媒介上。</p>

<p>可以说，对数据安全最重要的是第 3、4、5 步。即：</p>

<ul>
  <li>数据库软件多久来一次写系统调用，把它的用户空间 buffer 传输到内核空间 buffer？</li>
  <li>内核会多久来一次把 buffer 刷到磁盘控制器？</li>
  <li>磁盘控制器会多久把数据写到物理媒介一次？</li>
</ul>

<blockquote>
  <p>注意：当我们说<em>磁盘控制器的时候</em>，我们实际的意思是由控制器或者磁盘自己做的缓存。在持久化非常重要的系统中，管理员通常会把这个层面的缓存关掉。</p>

  <p>在大多数系统中，磁盘控制器默认仅通过缓存执行写操作。仅当你有电池，或者一个超级电容设备在停电时保护数据的情况下，打开写回模式（写缓存）才安全的。</p>
</blockquote>

<h2 id="posix-api">
<a class="anchor" href="#posix-api" aria-hidden="true"><span class="octicon octicon-link"></span></a>POSIX API</h2>

<p>从数据库开发者的角度看，数据被实际写入物理设备之前遵循的路径很有趣，更有趣的是这个路径上编程 API 提供的<em>控制数量</em>（TODO: ???）。</p>

<p>我们从第 3 步开始。我们使用<em>写</em>操作系统调用来把数据传输到内核 buffer，从这个角度看，我们使用 POSIX API 可以对此有一个很好的控制。不过，我们并不能控制这个系统调用要花多久才会成功返回。内核的写 buffer 大小是有限制的，如果磁盘不能应对应用程序的写带宽，那么内核写 buffer 就被写满，内核就会阻塞写操作。当磁盘能够接收到更多数据时，写系统调用才会返回。总之，目标是最终把数据写到物理媒介中。</p>

<p>在第 4 步中，内核把数据传送给磁盘控制器。通常操作系统会避免频繁这么做，因为大块地传输数据会更快。比如，Linux 默认会在 <strong>30 秒</strong>后才提交写操作。这意味着，一旦失败，最后 30 秒写的所有数据都可能会丢失。</p>

<p>POSIX API 提供了一族系统调用来强制内核把 buffer 写到磁盘：最有名的系统调用大概就是 <em><code class="language-plaintext highlighter-rouge">fsync</code></em> 了（可以在 <em>msync</em> 和 <em>fdatasync</em> 里查看更多信息）。使用 <code class="language-plaintext highlighter-rouge">fsync</code>，数据库系统就可以强制内核真的把数据提交到磁盘，不过你可以猜到，这是一个很昂贵的操作：每次被调用时，<strong><code class="language-plaintext highlighter-rouge">fsync</code> 都会初始化一个写操作</strong>，导致有一些数据在内核 buffer 中待处理。<code class="language-plaintext highlighter-rouge">fsync</code> 也会一直阻塞进程来完成写操作，在 Linux 上甚至会阻塞所有其他线程来避免写同一个文件。</p>

<h2 id="我们不能控制什么">
<a class="anchor" href="#%E6%88%91%E4%BB%AC%E4%B8%8D%E8%83%BD%E6%8E%A7%E5%88%B6%E4%BB%80%E4%B9%88" aria-hidden="true"><span class="octicon octicon-link"></span></a>我们不能控制什么？</h2>

<p>现在，我们学会了控制第 3 步和第 4 步，那第 5 步呢？坦白说，我们没办法使用 POSIX API 来控制这一步。可能有些内核实现会尝试告诉驱动器真地把数据提交到物理媒介上，或者控制器可能会为了速度，重排写操纵，导致数据没有被尽快写到磁盘上，而是等了几毫秒。这超出了我们的控制范围。</p>

<p>下面我们把场景限制为两个数据安全级别：</p>

<ul>
  <li>使用 <code class="language-plaintext highlighter-rouge">write(2)</code> 系统调用（或其他等价的方式）把数据写到内核 buffer 中，保证进程失败时的数据安全。</li>
  <li>使用 <code class="language-plaintext highlighter-rouge">fsync(2)</code> 系统调用（或其他等价的方式）把数据提交到磁盘，保证操作系统挂掉（比如断电）时的数据安全。我们当然知道这并不能完全保证数据安全，因为磁盘可能会有控制器缓存，不过我们不打算朝这方面考虑，因为这对于所有的数据库系统来说，都有这样的问题。另外，系统管理员通常会有专门的工具来精确控制物理设备的行为。</li>
</ul>

<blockquote>
  <p>注意：不是所有的数据库都使用 POSIX API。有些专用数据库会使用能够更加直接地和硬件打交道的内核模块。不过总体上，遇到的问题还是一样的。你可以使用用户空间 buffer、内核 buffer，不过数据只有写到磁盘上才真的安全（而这个操作通常很慢）。Oracle 就是使用内核模块。</p>
</blockquote>

<h2 id="数据崩溃">
<a class="anchor" href="#%E6%95%B0%E6%8D%AE%E5%B4%A9%E6%BA%83" aria-hidden="true"><span class="octicon octicon-link"></span></a>数据崩溃</h2>

<p>前面，我们在较高的层级——应用和内核——上分析了确保数据真的被传输到磁盘的问题。不过持久性并不仅考虑这些。接下来的问题是：如何保证数据库在灾难性时间发生后的可读性，或者数据内部结构因为某些原因崩溃了导致数据不能再被正确地读取，或者进行数据恢复来重建一个有效的数据表示？</p>

<p>很多 SQL 和 NoSQL 数据库实现了某种形式的磁盘上的树形数据结构用于存储数据和索引。这种数据结构在写的时候进行维护。如果在写操作的过程中系统停止工作了，树的表示还正确么？</p>

<p>一般地，在数据崩溃时有三个层面的安全：</p>

<ul>
  <li>数据库在往磁盘写数据的时候不关心失败时发生了什么，要求用户使用副本（<em>replica</em>）来恢复数据，或/并提供工具尽可能重建数据。</li>
  <li>数据库系统使用一个日志记录操作，这样在失败后可以从日志中恢复一致的状态。</li>
  <li>数据库系统绝不修改已经写入的数据，而是仅工作在<em>仅追加模式</em>，这样就不会崩溃。（可以看到，我们考虑的所谓崩溃，不是磁盘损坏之类的事情，而是程序原因导致的数据损坏）</li>
</ul>

<h2 id="参考">
<a class="anchor" href="#%E5%8F%82%E8%80%83" aria-hidden="true"><span class="octicon octicon-link"></span></a>参考</h2>

<ul>
  <li><a href="http://oldblog.antirez.com/post/redis-persistence-demystified.html">Redis persistence demystified</a></li>
</ul>
</article>
  </div>
  <div class="x-right">
    <h3>Redis</h3>
    
      <div class="x-collection-section">
        <h4>开始</h4>
        
        <ol>
          
            
              <li><a href="/redis/index.html">开始</a></li>
            
          
            
              <li><a href="/redis/install.html">安装</a></li>
            
          
            
              <li><a href="/redis/concept.html">相关概念预览</a></li>
            
          
        </ol>
      </div>
    
      <div class="x-collection-section">
        <h4>数据类型</h4>
        
        <ol>
          
            
              <li><a href="/redis/data-type/key-and-string.html">Key 和 String</a></li>
            
          
            
              <li><a href="/redis/data-type/list.html">List</a></li>
            
          
            
              <li><a href="/redis/data-type/hash.html">Hash</a></li>
            
          
            
              <li><a href="/redis/data-type/set.html">Set</a></li>
            
          
            
              <li><a href="/redis/data-type/zset.html">Sorted Set</a></li>
            
          
            
              <li><a href="/redis/data-type/bitmap.html">Bitmap</a></li>
            
          
            
              <li><a href="/redis/data-type/hyperloglog.html">Hyperloglog</a></li>
            
          
            
              <li><a href="/redis/data-type/geo.html">Geo</a></li>
            
          
        </ol>
      </div>
    
      <div class="x-collection-section">
        <h4>其他特性</h4>
        
        <ol>
          
            
              <li><a href="/redis/feature/pipeline.html">流水线处理</a></li>
            
          
        </ol>
      </div>
    
      <div class="x-collection-section">
        <h4>持久化</h4>
        
        <ol>
          
            
              <li class="active"><a href="/redis/persist/durability.html">持久化在说什么</a></li>
            
          
            
              <li><a href="/redis/persist/rdb.html">快照（Snapshotting）</a></li>
            
          
            
              <li><a href="/redis/persist/aof.html">AOF</a></li>
            
          
        </ol>
      </div>
    
      <div class="x-collection-section">
        <h4>运维</h4>
        
        <ol>
          
            
              <li><a href="/redis/ops/persistence.html">持久化方案</a></li>
            
          
        </ol>
      </div>
    
      <div class="x-collection-section">
        <h4>应用场景</h4>
        
        <ol>
          
            
              <li><a href="/redis/usecase/blogs.html">应用场景相关文章</a></li>
            
          
        </ol>
      </div>
    
  </div>
</div>
<footer class="site-footer h-card">
  <data class="u-url" href="/"></data>

  <div class="wrapper">

    <div class="footer-col-wrapper">
      <div class="footer-col">
        <p class="feed-subscribe">
          <a href="/feed.xml">
            <svg class="svg-icon orange">
              <use xlink:href="/assets/social-icons.svg#rss"></use>
            </svg><span>Subscribe</span>
          </a>
        </p>
      </div>
      <div class="footer-col">
        <p>Write an awesome description for your new site here. You can edit this line in _config.yml. It will appear in your document head meta (for Google search results) and in your feed.xml site description.</p>
      </div>
    </div>

    <div class="social-links"></div>

  </div>

</footer>
</body>
</html>
